Coverage Report

Created: 2025-11-02 11:31

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
D:\a\csshw\csshw\src\daemon\mod.rs
Line
Count
Source
1
//! Daemon imlementation
2
3
#![deny(clippy::implicit_return)]
4
#![allow(clippy::needless_return, clippy::doc_overindented_list_items)]
5
#![warn(missing_docs)]
6
7
use std::cmp::max;
8
use std::{
9
    io,
10
    sync::{Arc, Mutex},
11
    time::Duration,
12
};
13
use std::{thread, time};
14
15
use crate::get_console_window_handle;
16
use crate::utils::config::{Cluster, DaemonConfig};
17
use crate::utils::debug::StringRepr;
18
use crate::utils::windows::{clear_screen, set_console_color, WindowsApi};
19
use crate::{
20
    serde::{serialization::serialize_input_record_0, SERIALIZED_INPUT_RECORD_0_LENGTH},
21
    spawn_console_process,
22
    utils::{
23
        constants::{PIPE_NAME, PKG_NAME},
24
        windows::{
25
            arrange_console, get_console_input_buffer, read_keyboard_input,
26
            set_console_border_color,
27
        },
28
    },
29
    WindowsSettingsDefaultTerminalApplicationGuard,
30
};
31
use bracoxide::explode;
32
use log::{debug, error, warn};
33
use tokio::sync::broadcast::error::TryRecvError;
34
use tokio::{
35
    net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions},
36
    sync::broadcast::{self, Receiver, Sender},
37
    task::JoinHandle,
38
};
39
use windows::Win32::System::Console::{
40
    CONSOLE_CHARACTER_ATTRIBUTES, INPUT_RECORD_0, LEFT_CTRL_PRESSED, RIGHT_CTRL_PRESSED,
41
};
42
43
use windows::Win32::UI::Input::KeyboardAndMouse::{
44
    VIRTUAL_KEY, VK_A, VK_C, VK_E, VK_ESCAPE, VK_H, VK_R, VK_T,
45
};
46
use windows::Win32::UI::WindowsAndMessaging::{SW_RESTORE, SW_SHOWMINIMIZED};
47
use windows::Win32::{
48
    Foundation::{COLORREF, HANDLE, HWND, STILL_ACTIVE},
49
    System::{Console::ENABLE_PROCESSED_INPUT, Threading::PROCESS_QUERY_INFORMATION},
50
};
51
52
use self::workspace::WorkspaceArea;
53
54
mod workspace;
55
56
/// The capacity of the broadcast channel used
57
/// to send the input records read from the console input buffer
58
/// to the named pipe servers connected to each client in parallel.
59
const SENDER_CAPACITY: usize = 1024 * 1024;
60
61
/// Representation of a client
62
#[derive(Clone)]
63
struct Client {
64
    /// Hostname the client is connect to (or supposed to connect to).
65
    hostname: String,
66
    /// Window handle to the clients console window.
67
    window_handle: HWND,
68
    /// Process handle to the client process.
69
    process_handle: HANDLE,
70
}
71
72
unsafe impl Send for Client {}
73
74
/// Hacky wrapper around a window handle.
75
///
76
/// As we cannot implement foreign traits for foreign structs
77
/// we introduce this wrapper to implement [Send] for [HWND].
78
#[derive(Debug, Eq)]
79
struct HWNDWrapper {
80
    hwdn: HWND,
81
}
82
83
unsafe impl Send for HWNDWrapper {}
84
85
impl PartialEq for HWNDWrapper {
86
    /// Returns whether to `HWNDWrapper` instances are equal or not
87
    /// based on the [HWND] they wrap.
88
2
    fn eq(&self, other: &Self) -> bool {
89
2
        return self.hwdn == other.hwdn;
90
2
    }
91
}
92
93
/// Returns a window handle to the current console window.
94
///
95
/// The [HWND] is wrapped in a `HWNDWrapper` so that
96
/// we can pass it inbetween threads.
97
0
fn get_console_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper {
98
0
    return HWNDWrapper {
99
0
        hwdn: api.get_console_window(),
100
0
    };
101
0
}
102
103
/// Returns a window handle to the foreground window.
104
///
105
/// The [HWND] is wrapped in a `HWNDWrapper` so that
106
/// we can pass it inbetween threads.
107
0
fn get_foreground_window_wrapper(api: &dyn WindowsApi) -> HWNDWrapper {
108
0
    return HWNDWrapper {
109
0
        hwdn: api.get_foreground_window(),
110
0
    };
111
0
}
112
113
/// Enum of all possible control mode states.
114
#[derive(PartialEq, Debug)]
115
enum ControlModeState {
116
    /// Controle mode is inactive.
117
    Inactive,
118
    /// One of the keys required for the control mode key combination
119
    /// is currently being pressed.
120
    Initiated,
121
    /// All required keys for the control mode key combination were pressed
122
    /// and control mode is now active.
123
    ///
124
    /// Active control mode prevents any input records from being sent to clients.
125
    Active,
126
}
127
128
/// The daemon is responsible to launch a client for
129
/// each host, positioning the client windows, forwarding
130
/// input records to all clients and handling control mode.
131
struct Daemon<'a> {
132
    /// A list of hostnames to connect to.
133
    hosts: Vec<String>,
134
    /// A username to use to connect to all clients.
135
    ///
136
    /// If it is empty the clients will use the SSH config to find an approriate
137
    /// username.
138
    username: Option<String>,
139
    /// Optional port used for all SSH connections.
140
    port: Option<u16>,
141
    /// The `DaemonConfig` that controls how the daemon console window looks like.
142
    config: &'a DaemonConfig,
143
    /// List of available cluster tags
144
    clusters: &'a [Cluster],
145
    /// The current control mode state.
146
    control_mode_state: ControlModeState,
147
    /// If debug mode is enabled on the daemon it will also be enabled on all
148
    /// clients.
149
    debug: bool,
150
}
151
152
impl<'a> Daemon<'a> {
153
    /// Launches all client windows and blocks on the main run loop.
154
    ///
155
    /// Sets up the daemon console by disabling processed input mode and applying
156
    /// the configured colors and dimensions.
157
    /// Once all client windows have successfully started the daemon console window
158
    /// is moved to the foreground and receives focus.
159
0
    async fn launch<W: WindowsApi + Clone + 'static>(mut self, windows_api: &W) {
160
0
        windows_api
161
0
            .set_console_title(format!("{PKG_NAME} daemon").as_str())
162
0
            .unwrap();
163
0
        set_console_color(
164
0
            windows_api,
165
0
            CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color),
166
        );
167
0
        set_console_border_color(windows_api, COLORREF(0x000000FF));
168
169
0
        toggle_processed_input_mode(windows_api); // Disable processed input mode
170
171
        // Initialize the COM library so we can use UI automation
172
0
        windows_api
173
0
            .initialize_com_library(windows::Win32::System::Com::COINIT_MULTITHREADED)
174
0
            .unwrap();
175
176
0
        let workspace_area = workspace::get_workspace_area(windows_api, self.config.height);
177
178
0
        self.arrange_daemon_console(windows_api, &workspace_area);
179
180
        // Looks like on windows 10 re-arranging the console resets the console output buffer
181
0
        set_console_color(
182
0
            windows_api,
183
0
            CONSOLE_CHARACTER_ATTRIBUTES(self.config.console_color),
184
        );
185
186
0
        let mut clients = Arc::new(Mutex::new(
187
0
            launch_clients(
188
0
                windows_api,
189
0
                self.hosts.to_vec(),
190
0
                &self.username,
191
0
                self.port,
192
0
                self.debug,
193
0
                &workspace_area,
194
0
                self.config.aspect_ratio_adjustement,
195
0
                0,
196
0
            )
197
0
            .await,
198
        ));
199
200
        // Now that all clients started, focus the daemon console again.
201
0
        let daemon_console = windows_api.get_console_window();
202
0
        let _ = windows_api.set_foreground_window(daemon_console);
203
0
        let _ = windows_api.focus_window_with_automation(daemon_console);
204
205
0
        self.print_instructions(windows_api);
206
0
        self.run(windows_api, &mut clients, &workspace_area).await;
207
0
    }
208
209
    /// The main run loop of the `daemon` subcommand.
210
    ///
211
    /// Opens a multi-producer, multi-consumer broadcasting channel used to
212
    /// send the read input records in parallel to the name pipe servers
213
    /// the clients are listening on.
214
    /// Spawns a background thread that waits for all clients to terminate
215
    /// and then stops the current process.
216
    /// Spawns a background thread that ensures the z-order of all client
217
    /// windows is in sync with the daemon window.
218
    /// I.e. if the daemon window is focussed, all clients should be moved to the foreground.
219
    ///
220
    /// The main loop consists of waiting for input records to read from the keyboard,
221
    /// sending them to all clients and handling control mode.
222
    ///
223
    /// # Arguments
224
    ///
225
    /// * `windows_api`                     - The Windows API implementation to use
226
    /// * `clients`                         - A thread safe mapping from the number
227
    ///                                       a client console window was launched at
228
    ///                                       in relation to the other client windows
229
    ///                                       and the clients console window handle.
230
    /// * `workspace_area`                  - The available workspace area on the
231
    ///                                       primary monitor minus the space occupied
232
    ///                                       by the daemon console window.
233
0
    async fn run<W: WindowsApi + Clone + 'static>(
234
0
        &mut self,
235
0
        windows_api: &W,
236
0
        clients: &mut Arc<Mutex<Vec<Client>>>,
237
0
        workspace_area: &workspace::WorkspaceArea,
238
0
    ) {
239
0
        let (sender, _) =
240
0
            broadcast::channel::<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>(SENDER_CAPACITY);
241
242
0
        let mut servers = Arc::new(Mutex::new(self.launch_named_pipe_servers(&sender)));
243
244
        // Monitor client processes
245
0
        let clients_clone = Arc::clone(clients);
246
0
        let windows_api_clone = windows_api.clone();
247
0
        tokio::spawn(async move {
248
            loop {
249
0
                clients_clone.lock().unwrap().retain(|client| {
250
0
                    match windows_api_clone.get_exit_code(client.process_handle) {
251
0
                        Ok(exit_code) => return exit_code == STILL_ACTIVE.0 as u32,
252
0
                        Err(_) => return false, // Process handle is invalid, remove client
253
                    }
254
0
                });
255
0
                if clients_clone.lock().unwrap().is_empty() {
256
                    // All clients have exited, exit the daemon as well
257
0
                    std::process::exit(0);
258
0
                }
259
0
                tokio::time::sleep(Duration::from_millis(5)).await;
260
            }
261
        });
262
263
0
        ensure_client_z_order_in_sync_with_daemon(
264
0
            Arc::new(windows_api.clone()),
265
0
            clients.to_owned(),
266
        );
267
268
        loop {
269
0
            self.handle_input_record(
270
0
                windows_api,
271
0
                &sender,
272
0
                read_keyboard_input(windows_api),
273
0
                clients,
274
0
                workspace_area,
275
0
                &mut servers,
276
0
            )
277
0
            .await;
278
        }
279
    }
280
281
    /// Launch a named pipe server for each host in a dedicated thread.
282
    ///
283
    /// # Arguments
284
    ///
285
    /// * `sender` - The sender end of the broadcast channel through which
286
    ///              the main thread will send the input records that are to
287
    ///              be forwarded to the clients.
288
    ///
289
    /// # Returns
290
    ///
291
    /// Returns a list of [JoinHandle]s, one handle for each thread.
292
0
    fn launch_named_pipe_servers(
293
0
        &self,
294
0
        sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
295
0
    ) -> Vec<JoinHandle<()>> {
296
0
        let mut servers: Vec<JoinHandle<()>> = Vec::new();
297
0
        for _ in &self.hosts {
298
0
            self.launch_named_pipe_server(&mut servers, sender);
299
0
        }
300
0
        return servers;
301
0
    }
302
303
    /// Launch a named pipe server in a dedicated thread.
304
    ///
305
    /// # Arguments
306
    ///
307
    /// * `servers` - A list of [JoinHandle]s to which the join handle for
308
    ///               the new thread will be added.
309
    /// * `sender`  - The sender end of the broadcast channel through which
310
    ///               the main thread will send the input records that are to
311
    ///               be forwarded to the clients.
312
0
    fn launch_named_pipe_server(
313
0
        &self,
314
0
        servers: &mut Vec<JoinHandle<()>>,
315
0
        sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
316
0
    ) {
317
0
        let named_pipe_server = ServerOptions::new()
318
0
            .access_outbound(true)
319
0
            .pipe_mode(PipeMode::Message)
320
0
            .create(PIPE_NAME)
321
0
            .unwrap_or_else(|err| {
322
0
                error!("{}", err);
323
0
                panic!("Failed to create named pipe server",)
324
            });
325
0
        let mut receiver = sender.subscribe();
326
0
        servers.push(tokio::spawn(async move {
327
0
            named_pipe_server_routine(named_pipe_server, &mut receiver).await;
328
0
        }));
329
0
    }
330
331
    /// Handle the given input record.
332
    ///
333
    /// Input records are being forwarded to all clients.
334
    /// If a sequence of input records matches the control mode
335
    /// key combination, forwarding is temporarily interrupted,
336
    /// until control mode is exited.
337
    ///
338
    /// # Arguments
339
    ///
340
    /// * `sender`                          - The sender end of the broadcast channel
341
    ///                                       through which we will send the input records
342
    ///                                       that are being forwarded to the clients
343
    ///                                       by the named pipe servers (`servers`).
344
    /// * `input_record`                    - The [INPUT_RECORD_0].`KeyEvent` read from the
345
    ///                                       console input buffer.
346
    /// * `clients`                         - A thread safe mapping from the number
347
    ///                                       a client console window was launched at
348
    ///                                       in relation to the other client windows
349
    ///                                       and the clients console window handle.
350
    ///                                       The mapping will be extended if additional clients
351
    ///                                       are being added through control mode `[c]reate window(s)`.
352
    /// * `workspace_area`                  - The available workspace area on the
353
    ///                                       primary monitor minus the space occupied
354
    ///                                       by the daemon console window.
355
    /// * `servers`                         - A thread safe list of [JoinHandle]s,
356
    ///                                       one handle for each named pipe server background thread.
357
    ///                                       The list will be extended if additional clients are being added
358
    ///                                       through control mode `[c]reate window(s)`.
359
0
    async fn handle_input_record<W: WindowsApi + Clone + 'static>(
360
0
        &mut self,
361
0
        windows_api: &W,
362
0
        sender: &Sender<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
363
0
        input_record: INPUT_RECORD_0,
364
0
        clients: &mut Arc<Mutex<Vec<Client>>>,
365
0
        workspace_area: &workspace::WorkspaceArea,
366
0
        servers: &mut Arc<Mutex<Vec<JoinHandle<()>>>>,
367
0
    ) {
368
0
        if self.control_mode_is_active(windows_api, input_record) {
369
0
            if self.control_mode_state == ControlModeState::Initiated {
370
0
                clear_screen(windows_api);
371
0
                println!("Control Mode (Esc to exit)");
372
0
                println!("[c]reate window(s), [r]etile, copy active [h]ostname(s)");
373
0
                self.control_mode_state = ControlModeState::Active;
374
0
                return;
375
0
            }
376
0
            let key_event = unsafe { input_record.KeyEvent };
377
0
            if !key_event.bKeyDown.as_bool() {
378
0
                return;
379
0
            }
380
0
            match (
381
0
                VIRTUAL_KEY(key_event.wVirtualKeyCode),
382
0
                key_event.dwControlKeyState,
383
0
            ) {
384
0
                (VK_R, 0) => {
385
0
                    self.rearrange_client_windows(
386
0
                        windows_api,
387
0
                        &clients.lock().unwrap(),
388
0
                        workspace_area,
389
0
                    );
390
0
                    self.arrange_daemon_console(windows_api, workspace_area);
391
0
                }
392
0
                (VK_E, 0) => {
393
0
                    // TODO: Select windows
394
0
                }
395
0
                (VK_T, 0) => {
396
0
                    // TODO: trigger input on selected windows
397
0
                }
398
                (VK_C, 0) => {
399
0
                    clear_screen(windows_api);
400
                    // TODO: make ESC abort
401
0
                    println!("Hostname(s) or cluster tag(s): (leave empty to abort)");
402
0
                    toggle_processed_input_mode(windows_api); // As it was disabled before, this enables it again
403
0
                    let mut hostnames = String::new();
404
0
                    match io::stdin().read_line(&mut hostnames) {
405
0
                        Ok(2) => {
406
0
                            // Empty input (only newline '\n')
407
0
                        }
408
                        Ok(_) => {
409
0
                            let number_of_existing_clients = clients.lock().unwrap().len();
410
0
                            let new_clients = launch_clients(
411
0
                                windows_api,
412
0
                                resolve_cluster_tags(
413
0
                                    hostnames.split(' ').map(|x| return x.trim()).collect(),
414
0
                                    self.clusters,
415
                                )
416
0
                                .into_iter()
417
0
                                .map(|x| return x.to_owned())
418
0
                                .collect(),
419
0
                                &self.username,
420
0
                                self.port,
421
0
                                self.debug,
422
0
                                workspace_area,
423
0
                                self.config.aspect_ratio_adjustement,
424
0
                                number_of_existing_clients,
425
                            )
426
0
                            .await;
427
0
                            for client in new_clients.into_iter() {
428
0
                                clients.lock().unwrap().push(client);
429
0
                                self.launch_named_pipe_server(&mut servers.lock().unwrap(), sender);
430
0
                            }
431
                        }
432
0
                        Err(error) => {
433
0
                            error!("{error}");
434
                        }
435
                    }
436
0
                    toggle_processed_input_mode(windows_api); // Re-disable processed input mode.
437
0
                    self.rearrange_client_windows(
438
0
                        windows_api,
439
0
                        &clients.lock().unwrap(),
440
0
                        workspace_area,
441
                    );
442
0
                    self.arrange_daemon_console(windows_api, workspace_area);
443
                    // Focus the daemon console again.
444
0
                    let daemon_window = windows_api.get_console_window();
445
0
                    let _ = windows_api.set_foreground_window(daemon_window);
446
0
                    let _ = windows_api.focus_window_with_automation(daemon_window);
447
0
                    self.quit_control_mode(windows_api);
448
                }
449
                (VK_H, 0) => {
450
0
                    let mut active_hostnames: Vec<String> = vec![];
451
0
                    for client in clients.lock().unwrap().iter() {
452
0
                        if windows_api.is_window(client.window_handle) {
453
0
                            active_hostnames.push(client.hostname.clone());
454
0
                        }
455
                    }
456
0
                    cli_clipboard::set_contents(active_hostnames.join(" ")).unwrap();
457
0
                    self.quit_control_mode(windows_api);
458
                }
459
0
                _ => {}
460
            }
461
0
            return;
462
0
        }
463
0
        let error_handler = |err| {
464
0
            error!("{}", err);
465
0
            panic!(
466
0
                "Failed to serialize input recored `{}`",
467
0
                input_record.string_repr()
468
            )
469
        };
470
0
        match sender.send(
471
0
            serialize_input_record_0(&input_record)[..]
472
0
                .try_into()
473
0
                .unwrap_or_else(error_handler),
474
0
        ) {
475
0
            Ok(_) => {}
476
0
            Err(_) => {
477
0
                thread::sleep(time::Duration::from_nanos(1));
478
0
            }
479
        }
480
0
    }
481
482
    /// Returns whether control mode is active or not given the input_record.
483
    ///
484
    /// For control mode to be active this function needs to be called
485
    /// multiple times, as a key press translates to an input record and
486
    /// the key combination that activates control mode has 2 keys:
487
    /// `Ctrl + A`.
488
    /// The current control mode state is stored in `self.control_mode_state`.
489
    ///
490
    /// # Arguments
491
    ///
492
    /// * `windows_api` - The Windows API implementation to use
493
    /// * `input_record` -  A KeyEvent input record.
494
    ///
495
    /// # Returns
496
    ///
497
    /// Whether or not control mode is active.
498
0
    fn control_mode_is_active<W: WindowsApi>(
499
0
        &mut self,
500
0
        windows_api: &W,
501
0
        input_record: INPUT_RECORD_0,
502
0
    ) -> bool {
503
0
        let key_event = unsafe { input_record.KeyEvent };
504
0
        if self.control_mode_state == ControlModeState::Active {
505
0
            if key_event.wVirtualKeyCode == VK_ESCAPE.0 {
506
0
                self.quit_control_mode(windows_api);
507
0
                return false;
508
0
            }
509
0
            return true;
510
0
        }
511
0
        if (key_event.dwControlKeyState & LEFT_CTRL_PRESSED >= 1
512
0
            || key_event.dwControlKeyState & RIGHT_CTRL_PRESSED >= 1)
513
0
            && key_event.wVirtualKeyCode == VK_A.0
514
        {
515
0
            self.control_mode_state = ControlModeState::Initiated;
516
0
            return true;
517
0
        }
518
0
        return false;
519
0
    }
520
521
    /// Prints the default daemon instructions to the daemon console and
522
    /// sets `self.control_mode_state` to inactive.
523
0
    fn quit_control_mode<W: WindowsApi>(&mut self, windows_api: &W) {
524
0
        self.print_instructions(windows_api);
525
0
        self.control_mode_state = ControlModeState::Inactive;
526
0
    }
527
528
    /// Clears the console screen and prints the default daemon instructions.
529
0
    fn print_instructions<W: WindowsApi>(&self, windows_api: &W) {
530
0
        clear_screen(windows_api);
531
0
        println!("Input to terminal: (Ctrl-A to enter control mode)");
532
0
    }
533
534
    /// Iterates over all still open client windows and re-arranges them
535
    /// on the screen based on the aspect ration adjustment daemon configuration.
536
    ///
537
    /// Client windows will be re-sized and re-positioned.
538
    ///
539
    /// # Arguments
540
    ///
541
    /// * `windows_api`                     - The Windows API implementation to use
542
    /// * `clients`                         - A thread safe mapping from the number
543
    ///                                       a client console window was launched at
544
    ///                                       in relation to the other client windows
545
    ///                                       and the clients console window handle.
546
    ///                                       The number is relevant to determine the
547
    ///                                       position on the screen the window should
548
    ///                                       be placed at.
549
    /// * `workspace_area`                  - The available workspace area on the
550
    ///                                       primary monitor minus the space occupied
551
    ///                                       by the daemon console window.
552
0
    fn rearrange_client_windows<W: WindowsApi>(
553
0
        &self,
554
0
        windows_api: &W,
555
0
        clients: &[Client],
556
0
        workspace_area: &workspace::WorkspaceArea,
557
0
    ) {
558
0
        let mut valid_clients = Vec::new();
559
0
        for client in clients.iter() {
560
0
            let exit_code = match windows_api.get_exit_code(client.process_handle) {
561
0
                Ok(code) => code,
562
0
                Err(_) => continue, // Process handle is invalid, skip client
563
            };
564
0
            if exit_code == STILL_ACTIVE.0 as u32 && windows_api.is_window(client.window_handle) {
565
0
                valid_clients.push(client);
566
0
            }
567
        }
568
0
        for (index, client) in valid_clients.iter().enumerate() {
569
0
            arrange_client_window(
570
0
                windows_api,
571
0
                &client.window_handle,
572
0
                workspace_area,
573
0
                index,
574
0
                valid_clients.len(),
575
0
                self.config.aspect_ratio_adjustement,
576
            )
577
        }
578
0
    }
579
580
    /// Re-sizes and re-positions the daemon console window on the screen
581
    /// based on the daemon height configuration.
582
    ///
583
    /// # Arguments
584
    ///
585
    /// * `windows_api` - The Windows API implementation to use
586
    /// * `workspace_area` - The available workspace area on the
587
    ///                      primary monitor minus the space occupied
588
    ///                      by the daemon console window.
589
0
    fn arrange_daemon_console<W: WindowsApi>(
590
0
        &self,
591
0
        windows_api: &W,
592
0
        workspace_area: &WorkspaceArea,
593
0
    ) {
594
0
        let (x, y, width, height) = get_console_rect(
595
0
            0,
596
0
            workspace_area.height,
597
0
            workspace_area.width - (workspace_area.x_fixed_frame + workspace_area.x_size_frame),
598
0
            self.config.height,
599
0
            workspace_area,
600
0
        );
601
0
        arrange_console(windows_api, x, y, width, height);
602
0
    }
603
}
604
605
/// The processed console input mode controls whether special key combinations
606
/// such as `Ctrl + c` or `Ctrl + BREAK` receive special handling or are treated
607
/// as simple key presses.
608
///
609
/// By default processed input mode is enabled, meaning `Ctrl + c` is treated as
610
/// a signal, not key presses.
611
///
612
/// <https://learn.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals>
613
///
614
/// # Arguments
615
///
616
/// * `windows_api` - The Windows API implementation to use
617
0
fn toggle_processed_input_mode<W: WindowsApi>(windows_api: &W) {
618
0
    let handle = get_console_input_buffer();
619
0
    let mode = windows_api.get_console_mode(handle).unwrap();
620
0
    let new_mode = windows::Win32::System::Console::CONSOLE_MODE(mode.0 ^ ENABLE_PROCESSED_INPUT.0);
621
0
    let _ = windows_api.set_console_mode(handle, new_mode);
622
0
}
623
624
/// Resolve cluster tags into hostnames
625
///
626
/// Iterates over the list of hosts to find and resolve cluster tags.
627
/// Nested cluster tags are supported but recursivness is not checked for.
628
///
629
/// # Arguments
630
///
631
/// * `hosts`       - List of hosts including hostnames and or cluster tags
632
/// * `clusters`    - List of available cluster tags
633
///
634
/// # Returns
635
///
636
/// A list of hostnames
637
6
pub fn resolve_cluster_tags<'a>(hosts: Vec<&'a str>, clusters: &'a [Cluster]) -> Vec<&'a str> {
638
6
    let mut resolved_hosts: Vec<&str> = Vec::new();
639
    let mut is_cluster_tag: bool;
640
19
    for 
host13
in hosts {
641
13
        is_cluster_tag = false;
642
27
        for 
cluster17
in clusters {
643
17
            if host == cluster.name {
644
3
                is_cluster_tag = true;
645
3
                resolved_hosts.extend(resolve_cluster_tags(
646
6
                    
cluster.hosts.iter()3
.
map3
(|host| return &**host).
collect3
(),
647
3
                    clusters,
648
                ));
649
3
                break;
650
14
            }
651
        }
652
13
        if !is_cluster_tag {
653
10
            resolved_hosts.push(host);
654
10
        
}3
655
    }
656
6
    return resolved_hosts;
657
6
}
658
659
/// Launches a client console for each given host and waits for
660
/// the client windows to exist before returning their handles.
661
///
662
/// # Arguments
663
///
664
/// * `windows_api`             - The Windows API implementation to use
665
/// * `hosts`                   - List of hosts
666
/// * `username`                - Optional username, if none is given
667
///                               the client will use the SSH config to
668
///                               determine a username.
669
/// * `port`                    - Optional port for SSH connections
670
/// * `debug`                   - Toggles debug mode on the client.
671
/// * `workspace_area`          - The available workspace area on the primary monitor
672
///                               minus the space occupied by the daemon console window.
673
///                               Used to arrange the client window.
674
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
675
///                               Used to arrange the client window.
676
/// * `index_offset`            - Offset used to position the new windows correctly
677
///                               from the start, avoiding flickering.
678
///
679
/// # Returns
680
///
681
/// A mapping from the order a client console window was launched at
682
/// in relation to the other client windows and the clients console window handle.
683
0
async fn launch_clients<W: WindowsApi + 'static + Clone>(
684
0
    windows_api: &W,
685
0
    hosts: Vec<String>,
686
0
    username: &Option<String>,
687
0
    port: Option<u16>,
688
0
    debug: bool,
689
0
    workspace_area: &workspace::WorkspaceArea,
690
0
    aspect_ratio_adjustment: f64,
691
0
    index_offset: usize,
692
0
) -> Vec<Client> {
693
0
    let len_hosts = hosts.len();
694
0
    let _guard = WindowsSettingsDefaultTerminalApplicationGuard::new();
695
696
    // Create an Arc to share the windows_api across parallel tasks
697
0
    let windows_api_arc = Arc::new(windows_api.clone());
698
699
    // Create tasks for each client launch using spawn_blocking to handle the synchronous operations
700
0
    let mut tasks = Vec::new();
701
702
0
    for (index, host) in hosts.into_iter().enumerate() {
703
0
        let username_client = username.clone();
704
0
        let workspace_area_client = *workspace_area;
705
0
        let windows_api_clone = Arc::clone(&windows_api_arc);
706
707
        // Use spawn_blocking to run the synchronous launch_client_console in parallel
708
0
        let task = tokio::task::spawn_blocking(move || {
709
0
            let (window_handle, process_handle) = launch_client_console(
710
0
                windows_api_clone.as_ref(),
711
0
                &host,
712
0
                username_client,
713
0
                port,
714
0
                debug,
715
0
                index + index_offset,
716
0
                &workspace_area_client,
717
0
                len_hosts + index_offset,
718
0
                aspect_ratio_adjustment,
719
0
            );
720
0
            return (
721
0
                index,
722
0
                Client {
723
0
                    hostname: host,
724
0
                    window_handle,
725
0
                    process_handle,
726
0
                },
727
0
            );
728
0
        });
729
730
0
        tasks.push(task);
731
    }
732
733
    // Wait for all tasks to complete in parallel
734
0
    let mut results = Vec::new();
735
0
    for task in tasks {
736
0
        match task.await {
737
0
            Ok(result) => results.push(result),
738
0
            Err(e) => panic!("Failed to launch client: {e}"),
739
        }
740
    }
741
742
    // Sort results by index to maintain order
743
0
    results.sort_by_key(|(index, _)| return *index);
744
745
0
    return results
746
0
        .into_iter()
747
0
        .map(|(_, client)| return client)
748
0
        .collect();
749
0
}
750
751
/// Launchs a `client` console process with its own window with the given
752
/// CLI arguments/options: `host`, `username`, `port`, `debug`.
753
///
754
/// Waits for the window to open, then re-arranges it based on
755
/// the total number of clients, the size of the daemon console window and
756
/// its index relative to the other client windows.
757
///
758
/// # Arguments
759
///
760
/// * `windows_api`             - The Windows API implementation to use
761
/// * `host`                    - Hostname the client should connect to
762
/// * `username`                - Username the client should use
763
/// * `port`                    - Optional port for SSH connections
764
/// * `debug`                   - Toggle debug mode on the client
765
/// * `index`                   - The index of the client in the list of all clients.
766
///                               Used to re-arrange the client window.
767
/// * `workspace_area`          - The available workspace area on the primary monitor
768
///                               minus the space occupied by the daemon console window.
769
/// * `number_of_consoles`      - The total number of active client console windows.
770
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
771
///
772
/// # Returns
773
///
774
/// A tuple containing the window handle and process handle of the client process.
775
0
fn launch_client_console<W: WindowsApi>(
776
0
    windows_api: &W,
777
0
    host: &str,
778
0
    username: Option<String>,
779
0
    port: Option<u16>,
780
0
    debug: bool,
781
0
    index: usize,
782
0
    workspace_area: &workspace::WorkspaceArea,
783
0
    number_of_consoles: usize,
784
0
    aspect_ratio_adjustment: f64,
785
0
) -> (HWND, HANDLE) {
786
    // The first argument must be `--` to ensure all following arguments are treated
787
    // as positional arguments and not as options if they start with `-`.
788
0
    let mut client_args: Vec<String> = Vec::new();
789
0
    if debug {
790
0
        client_args.push("-d".to_string());
791
0
    }
792
0
    let mut actual_host = host;
793
0
    let mut actual_username = username;
794
0
    if let Some(split_result) = host.split_once("@") {
795
0
        actual_username = Some(split_result.0.to_owned());
796
0
        actual_host = split_result.1;
797
0
    }
798
0
    if let Some(actual_username) = actual_username.as_deref() {
799
0
        client_args.extend(vec!["-u".to_string(), actual_username.to_string()]);
800
0
    }
801
0
    if let Some(port) = port {
802
0
        client_args.extend(vec!["-p".to_string(), port.to_string()]);
803
0
    }
804
0
    client_args.push("client".to_string());
805
0
    client_args.extend(vec!["--".to_string(), actual_host.to_string()]);
806
807
0
    let process_info = spawn_console_process(windows_api, &format!("{PKG_NAME}.exe"), client_args)
808
0
        .expect("Failed to create process");
809
0
    let client_window_handle = get_console_window_handle(windows_api, process_info.dwProcessId);
810
0
    let process_handle = windows_api
811
0
        .open_process(PROCESS_QUERY_INFORMATION.0, false, process_info.dwProcessId)
812
0
        .unwrap_or_else(|err| {
813
0
            panic!(
814
0
                "Failed to open process handle for process {}: {}",
815
                process_info.dwProcessId, err
816
            );
817
        });
818
819
0
    arrange_client_window(
820
0
        windows_api,
821
0
        &client_window_handle,
822
0
        workspace_area,
823
0
        index,
824
0
        number_of_consoles,
825
0
        aspect_ratio_adjustment,
826
    );
827
0
    return (client_window_handle, process_handle);
828
0
}
829
830
/// Wait for the named pipe server to connect, then forward serialized
831
/// input records read from the broadcast channel to the named pipe server.
832
///
833
/// If writing to the pipe fails the pipe is closed and the routine ends.
834
/// To detect if a client is still alive even if we are currently
835
/// not sending data, we send a "keep alive packet",
836
/// [`SERIALIZED_INPUT_RECORD_0_LENGTH`] bytes of `1`s. If that fails, the routine ends.
837
///
838
/// # Arguments
839
///
840
/// * `server`   - The named pipe server over which we send data to the
841
///                client.
842
/// * `receiver` - The receiving end of the broadcast channel through
843
///                which we get the serialize input records from the main
844
///                thread that are to be sent to the client via the named
845
///                pipe.
846
2
async fn named_pipe_server_routine(
847
2
    server: NamedPipeServer,
848
2
    receiver: &mut Receiver<[u8; SERIALIZED_INPUT_RECORD_0_LENGTH]>,
849
2
) {
850
    // wait for a client to connect
851
2
    server.connect().await.unwrap_or_else(|err| 
{0
852
0
        error!("{}", err);
853
0
        panic!("Timeded out waiting for clients to connect to named pipe server",)
854
    });
855
    loop {
856
11
        let 
ser_input_record8
= match receiver.try_recv() {
857
8
            Ok(val) => val,
858
            Err(TryRecvError::Empty) => {
859
2
                tokio::time::sleep(Duration::from_millis(5)).await;
860
                // Try sending dummy data to detect early if the pipe is closed because the client exited
861
2
                match server.try_write(&[u8::MAX; SERIALIZED_INPUT_RECORD_0_LENGTH]) {
862
2
                    Ok(_) => continue,
863
0
                    Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue,
864
                    Err(_) => {
865
0
                        debug!(
866
0
                            "Named pipe server ({:?}) is closed, stopping named pipe server routine",
867
                            server
868
                        );
869
0
                        return;
870
                    }
871
                }
872
            }
873
1
            Err(err) => {
874
1
                error!(
"{}"0
, err);
875
1
                panic!("Failed to receive data from the Receiver");
876
            }
877
        };
878
        loop {
879
14
            server.writable().await.unwrap_or_else(|err| 
{0
880
0
                error!("{}", err);
881
0
                panic!("Timed out waiting for named pipe server to become writable",)
882
            });
883
14
            match server.try_write(&ser_input_record) {
884
                Ok(SERIALIZED_INPUT_RECORD_0_LENGTH) => {
885
7
                    debug!(
"Successfully written all data"0
);
886
7
                    break;
887
                }
888
0
                Ok(n) => {
889
                    // The data was only written partially, try again
890
0
                    warn!(
891
0
                        "Partially written data, expected {} but only wrote {}",
892
                        SERIALIZED_INPUT_RECORD_0_LENGTH, n
893
                    );
894
0
                    continue;
895
                }
896
7
                Err(
e6
) if e.kind() == io::ErrorKind::WouldBloc
k6
=> {
897
                    // Try again
898
6
                    debug!(
"Writing to named pipe server would have blocked"0
);
899
6
                    continue;
900
                }
901
                Err(_) => {
902
                    // Can happen if the pipe is closed because the
903
                    // client exited
904
1
                    debug!(
905
0
                        "Named pipe server ({:?}) is closed, stopping named pipe server routine",
906
                        server
907
                    );
908
1
                    return;
909
                }
910
            }
911
        }
912
    }
913
1
}
914
915
/// Re-sizes and re-positions the given client window based on the total number of clients,
916
/// the size of the daemon console window and its index relative to the other client windows.
917
///
918
/// # Arguments
919
///
920
/// * `windows_api`              - The Windows API implementation to use
921
/// * `handle`                   - Reference the windows handle of a client console window.
922
/// * `workspace_area`           - The available workspace area on the primary monitor
923
///                                minus the space occupied by the daemon console window.
924
/// * `index`                    - The index of the client in the list of all clients.
925
/// * `number_of_consoles`       - The total number of active client console windows.
926
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
927
0
fn arrange_client_window<W: WindowsApi>(
928
0
    windows_api: &W,
929
0
    handle: &HWND,
930
0
    workspace_area: &workspace::WorkspaceArea,
931
0
    index: usize,
932
0
    number_of_consoles: usize,
933
0
    aspect_ratio_adjustment: f64,
934
0
) {
935
0
    let (x, y, width, height) = determine_client_spatial_attributes(
936
0
        index as i32,
937
0
        number_of_consoles as i32,
938
0
        workspace_area,
939
0
        aspect_ratio_adjustment,
940
0
    );
941
    // Since windows update 10.0.19041.5072 it can happen that a client windows rendering is broken
942
    // after a move+resize. Why is unclear, but resizing again does solve the issue.
943
    // We first make the window 1 pixel in each dimension too small and imediately fix it.
944
    // To reduce overhead we do not repaint the window the first time.
945
0
    windows_api
946
0
        .move_window(*handle, x, y, width - 1, height - 1, false)
947
0
        .unwrap_or_else(|err| {
948
0
            error!("{}", err);
949
0
            panic!("Failed to move window",)
950
        });
951
0
    windows_api
952
0
        .move_window(*handle, x, y, width, height, true)
953
0
        .unwrap_or_else(|err| {
954
0
            error!("{}", err);
955
0
            panic!("Failed to move window",)
956
        });
957
0
}
958
959
/// Calculates the position and dimensions for a client window given its index,
960
/// the total number of clients and the `aspect_ratio_adjustment` daemon configuration.
961
///
962
/// # Arguments
963
///
964
/// * `index`                    - The index of the client in the list of all clients.
965
/// * `number_of_consoles`       - The total number of active client console windows.
966
/// * `workspace_area`           - The available workspace area on the primary monitor
967
///                                minus the space occupied by the daemon console window.
968
/// * `aspect_ratio_adjustment` - The `aspect_ratio_adjustment` daemon configuration.
969
///     * `> 0.0` - Aims for vertical rectangle shape.
970
///       The larger the value, the more exaggerated the "verticality".
971
///       Eventually the windows will all be columns.
972
///     * `= 0.0` - Aims for square shape.
973
///     * `< 0.0` - Aims for horizontal rectangle shape.
974
///       The smaller the value, the more exaggerated the "horizontality".
975
///       Eventually the windows will all be rows.
976
///       `-1.0` is the sweetspot for mostly preserving a 16:9 ratio.
977
0
fn determine_client_spatial_attributes(
978
0
    index: i32,
979
0
    number_of_consoles: i32,
980
0
    workspace_area: &workspace::WorkspaceArea,
981
0
    aspect_ratio_adjustment: f64,
982
0
) -> (i32, i32, i32, i32) {
983
0
    let aspect_ratio = (workspace_area.width
984
0
        + (workspace_area.x_fixed_frame + workspace_area.x_size_frame) * 2)
985
0
        as f64
986
0
        / (workspace_area.height + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * 2)
987
0
            as f64;
988
989
0
    let grid_columns = max(
990
0
        ((number_of_consoles as f64).sqrt() * (aspect_ratio + aspect_ratio_adjustment)) as i32,
991
        1,
992
    );
993
0
    let grid_rows = max(
994
0
        (number_of_consoles as f64 / grid_columns as f64).ceil() as i32,
995
        1,
996
    );
997
998
0
    let grid_column_index = index % grid_columns;
999
0
    let grid_row_index = index / grid_columns;
1000
1001
0
    let is_last_row = grid_row_index == grid_rows - 1;
1002
0
    let last_row_console_count = number_of_consoles % grid_columns;
1003
1004
0
    let console_width = if is_last_row && last_row_console_count != 0 {
1005
0
        (workspace_area.width / last_row_console_count)
1006
0
            + if last_row_console_count > 1 {
1007
0
                workspace_area.x_fixed_frame + workspace_area.x_size_frame
1008
            } else {
1009
0
                0
1010
            }
1011
    } else {
1012
0
        (workspace_area.width / grid_columns)
1013
0
            + (workspace_area.x_fixed_frame + workspace_area.x_size_frame)
1014
    };
1015
1016
0
    let console_height = (workspace_area.height
1017
0
        + (workspace_area.y_fixed_frame + workspace_area.y_size_frame) * grid_row_index)
1018
0
        / grid_rows;
1019
1020
0
    let x = grid_column_index * console_width
1021
0
        - ((workspace_area.x_fixed_frame + workspace_area.x_size_frame) * (grid_column_index + 1));
1022
0
    let y = grid_row_index * console_height
1023
0
        - ((workspace_area.y_fixed_frame + workspace_area.y_size_frame) * (grid_row_index - 1));
1024
1025
0
    return get_console_rect(x, y, console_width, console_height, workspace_area);
1026
0
}
1027
1028
/// Transform the position and dimensions of a console window based
1029
/// on the workspace area.
1030
///
1031
/// To minimize empty space between windows, width and height must be adjusted
1032
/// by the `fixed_frame` and `size_frame` values.
1033
///
1034
/// # Arguments
1035
///
1036
/// * `x`              - The `x` coordinate of the window.
1037
/// * `y`              - The `y` coordinate of the window.
1038
/// * `width`          - The `width` in pixels of the window.
1039
/// * `height`         - The `height` in pixels of the window.
1040
/// * `workspace_area` - The available workspace area on the primary monitor minus
1041
///                      the space occupied by the daemon console window.
1042
///
1043
/// # Returns
1044
///
1045
/// (`x`, `y`, `width`, `height`)
1046
///
1047
0
fn get_console_rect(
1048
0
    x: i32,
1049
0
    y: i32,
1050
0
    width: i32,
1051
0
    height: i32,
1052
0
    workspace_area: &workspace::WorkspaceArea,
1053
0
) -> (i32, i32, i32, i32) {
1054
0
    return (
1055
0
        std::cmp::max(
1056
0
            workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame),
1057
0
            workspace_area.x - (workspace_area.x_fixed_frame + workspace_area.x_size_frame) + x,
1058
0
        ),
1059
0
        workspace_area.y - (workspace_area.y_fixed_frame + workspace_area.y_size_frame) + y,
1060
0
        std::cmp::min(workspace_area.width, width),
1061
0
        height,
1062
0
    );
1063
0
}
1064
1065
/// Spawns a background thread that ensures the z-order of all client
1066
/// windows is in sync with the daemon window.
1067
/// I.e. if the daemon window is focussed, all clients should be moved to the foreground.
1068
///
1069
/// # Arguments
1070
///
1071
/// * `windows_api` - Arc-wrapped Windows API implementation for thread-safe access
1072
/// * `clients`     - A thread safe mapping from the number
1073
///                   a client console window was launched at
1074
///                   in relation to the other client windows
1075
///                   and the clients console window handle.
1076
///                   The mapping must be thread safe to allow
1077
///                   it to be modified by the main thread
1078
///                   while we periodically read from it in the
1079
///                   background thread.
1080
0
fn ensure_client_z_order_in_sync_with_daemon<W: WindowsApi + Send + Sync + 'static>(
1081
0
    windows_api: Arc<W>,
1082
0
    clients: Arc<Mutex<Vec<Client>>>,
1083
0
) {
1084
0
    tokio::spawn(async move {
1085
0
        let daemon_handle = get_console_window_wrapper(windows_api.as_ref());
1086
0
        let mut previous_foreground_window = get_foreground_window_wrapper(windows_api.as_ref());
1087
        loop {
1088
0
            tokio::time::sleep(Duration::from_millis(1)).await;
1089
0
            let foreground_window = get_foreground_window_wrapper(windows_api.as_ref());
1090
0
            if previous_foreground_window == foreground_window {
1091
0
                continue;
1092
0
            }
1093
0
            if foreground_window == daemon_handle
1094
0
                && !clients.lock().unwrap().iter().any(|client| {
1095
0
                    return client.window_handle == previous_foreground_window.hwdn
1096
0
                        || client.window_handle == daemon_handle.hwdn;
1097
0
                })
1098
0
            {
1099
0
                defer_windows(
1100
0
                    windows_api.as_ref(),
1101
0
                    &clients.lock().unwrap(),
1102
0
                    &daemon_handle.hwdn,
1103
0
                );
1104
0
            }
1105
0
            previous_foreground_window = foreground_window;
1106
        }
1107
    });
1108
0
}
1109
1110
/// Move all given windows to the foreground.
1111
///
1112
/// Restores minimized windows.
1113
/// If a window handle no longer points to a valid window, it is skipped.
1114
/// The daemon window is deferred last and receives focus.
1115
///
1116
/// # Arguments
1117
///
1118
/// * `windows_api`                   - The Windows API implementation to use
1119
/// * `clients`                       - A thread safe mapping from the number
1120
///                                     a client console window was launched at
1121
///                                     in relation to the other client windows
1122
///                                     and the clients console window handle.
1123
/// * `daemon_handle`                 - Handle to the daemon console window.
1124
0
fn defer_windows<W: WindowsApi>(windows_api: &W, clients: &[Client], daemon_handle: &HWND) {
1125
0
    for client in clients.iter().chain([&Client {
1126
0
        hostname: "root".to_owned(),
1127
0
        window_handle: *daemon_handle,
1128
0
        process_handle: HANDLE::default(),
1129
0
    }]) {
1130
0
        let placement = match windows_api.get_window_placement(client.window_handle) {
1131
0
            Ok(placement) => placement,
1132
            Err(_) => {
1133
0
                continue;
1134
            }
1135
        };
1136
        // First restore if window is minimized
1137
0
        if placement.showCmd == SW_SHOWMINIMIZED.0.try_into().unwrap() {
1138
0
            let _ = windows_api.show_window(client.window_handle, SW_RESTORE);
1139
0
        }
1140
        // Then bring it to front using UI automation
1141
0
        let _ = windows_api.focus_window_with_automation(client.window_handle);
1142
    }
1143
0
}
1144
1145
/// The entrypoint for the `daemon` subcommand.
1146
///
1147
/// Spawns 1 client process with its own window for each host
1148
/// and 1 worker thread that handles communication with the client
1149
/// over a named pipe.
1150
/// Responsible for client window positioning and sizing.
1151
/// Handles control mode.
1152
/// Main thread reads input records from the console input buffer
1153
/// and propagates them via the background threads to all clients
1154
/// simultaneously.
1155
///
1156
/// # Arguments
1157
///
1158
/// * `windows_api` - The Windows API implementation to use
1159
/// * `hosts`    - List of hostnames for which to launch clients.
1160
/// * `username` - Username used to connect to the hosts.
1161
///                If none, each client will use the SSH config to determine
1162
///                a suitable username for their respective host.
1163
/// * `port`     - Optional port used for all SSH connections.
1164
/// * `config`   - The `DaemonConfig`.
1165
/// * `debug`    - Enables debug logging
1166
0
pub async fn main<W: WindowsApi + Clone + 'static>(
1167
0
    windows_api: &W,
1168
0
    hosts: Vec<String>,
1169
0
    username: Option<String>,
1170
0
    port: Option<u16>,
1171
0
    config: &DaemonConfig,
1172
0
    clusters: &[Cluster],
1173
0
    debug: bool,
1174
0
) {
1175
0
    let daemon: Daemon = Daemon {
1176
0
        hosts: explode(&hosts.join(" ")).unwrap_or(hosts),
1177
0
        username,
1178
0
        port,
1179
0
        config,
1180
0
        clusters,
1181
0
        control_mode_state: ControlModeState::Inactive,
1182
0
        debug,
1183
0
    };
1184
0
    daemon.launch(windows_api).await;
1185
0
    debug!("Actually exiting");
1186
0
}
1187
1188
#[cfg(test)]
1189
#[path = "../tests/daemon/test_mod.rs"]
1190
mod test_mod;